// noipnoi方案，非常好的方案！！
#include <bits/stdc++.h>

using namespace std;
#define ll long long
const int MAX = 300300;
#define lson (x << 1)
#define rson (x << 1 | 1)
int n, Q;

/**
 * 集合大小统计线段树：维护每个集合的元素数量统计信息
 * 功能：记录每个集合当前包含的元素个数，支持区间加减操作和区间最大值查询
 */
struct SetSizeStatisticsTree {
  int lazy_tag[MAX << 2], max_size[MAX << 2],
      max_count[MAX << 2]; // 懒标记、最大值、最大值出现次数
  // max_size[节点]: 该节点所代表区间内所有集合元素数量的最大值
  // max_count[节点]: 该区间内达到最大元素数量的集合个数

  /**
   * 上推操作：合并左右子树信息
   * 当左右子树最大值相等时，合并计数；否则取较大值
   */
  void pushup(int x) {
    if (max_size[lson] == max_size[rson]) {
      max_size[x] = max_size[lson];
      max_count[x] = max_count[lson] + max_count[rson];
    } else if (max_size[lson] < max_size[rson]) {
      max_size[x] = max_size[rson];
      max_count[x] = max_count[rson];
    } else {
      max_size[x] = max_size[lson];
      max_count[x] = max_count[lson];
    }
  }

  /**
   * 构建线段树
   * 初始化时每个集合元素数量为0，计数为1（每个叶子节点代表一个集合）
   */
  void Build(int x, int l, int r) {
    lazy_tag[x] = 0;
    if (l == r) {
      max_count[x] = 1; // 达到最大值0的集合个数就是1个
      max_size[x] = 0;  // 初始元素数量为0
      return;
    }
    int mid = (l + r) >> 1;
    Build(lson, l, mid);
    Build(rson, mid + 1, r);
    pushup(x);
  }

  /**
   * 添加懒标记
   * 对当前节点进行区间加减操作
   */
  void puttag(int x, int k) {
    lazy_tag[x] += k;
    max_size[x] += k;
  }

  /**
   * 下推懒标记到子节点
   */
  void pushdown(int x) {
    if (lazy_tag[x] == 0)
      return;
    puttag(lson, lazy_tag[x]);
    puttag(rson, lazy_tag[x]);
    lazy_tag[x] = 0;
  }

  /**
   * 区间修改操作
   * 对区间[L,R]的所有集合元素数量增加k
   * 对应题意：添加或删除元素时更新集合大小
   */
  void modify(int x, int l, int r, int L, int R, int k) {
    if (L <= l && r <= R) {
      puttag(x, k);
      return;
    }
    pushdown(x);
    int mid = (l + r) >> 1;
    if (L <= mid)
      modify(lson, l, mid, L, R, k);
    if (R > mid)
      modify(rson, mid + 1, r, L, R, k);
    pushup(x);
  }

  /**
   * 区间查询操作
   * 查询区间[L,R]内集合元素数量的最大值和达到该最大值的集合个数
   * 对应题意：类型3查询
   */
  pair<int, int> Query(int x, int l, int r, int L, int R) {
    if (L <= l && r <= R)
      return make_pair(max_size[x], max_count[x]);

    pushdown(x);
    int mid = (l + r) >> 1;

    if (R <= mid) {
      return Query(lson, l, mid, L, R);
    } else if (L > mid) {
      return Query(rson, mid + 1, r, L, R);
    } else {
      pair<int, int> ql = Query(lson, l, mid, L, mid);
      pair<int, int> qr = Query(rson, mid + 1, r, mid + 1, R);
      if (ql.first == qr.first)
        return make_pair(ql.first, ql.second + qr.second);
      else
        return (ql.first > qr.first) ? ql : qr;
    }
  }
} set_size_tree;

/**
 * 元素存在状态线段树：维护每个值在哪些集合中存在
 * 对于每个值x（1-60），维护一个线段树，记录该值在哪些集合中
 * contained_count[x]表示区间内包含该值的集合数量，lazy_tag[x]表示懒标记
 */
struct ElementExistenceTree {
  int contained_count[MAX << 2], lazy_tag[MAX << 2]; // 区间和、懒标记

  void pushup(int x) {
    contained_count[x] = contained_count[lson] + contained_count[rson];
  }

  /**
   * 构建线段树
   * 初始时所有集合都不包含该值，所以contained_count[x]=0
   */
  void Build(int x, int l, int r) {
    lazy_tag[x] = -1; // -1表示无懒标记
    if (l == r)
      return;
    int mid = (l + r) >> 1;
    Build(lson, l, mid);
    Build(rson, mid + 1, r);
    pushup(x);
  }

  /**
   * 设置懒标记：将整个区间设置为k（0或1）
   * k=1表示该区间所有集合都包含该值
   * k=0表示该区间所有集合都不包含该值
   */
  void puttag(int x, int l, int r, int k) {
    contained_count[x] = k * (r - l + 1);
    lazy_tag[x] = k;
  }

  void pushdown(int x, int l, int r) {
    if (lazy_tag[x] == -1)
      return;
    int mid = (l + r) >> 1;
    puttag(lson, l, mid, lazy_tag[x]);
    puttag(rson, mid + 1, r, lazy_tag[x]);
    lazy_tag[x] = -1;
  }

  /**
   * 区间修改操作
   * 将区间[L,R]内所有集合对该值的包含状态设置为k
   * k=1：添加该值到集合
   * k=0：从集合中删除该值
   */
  void modify(int x, int l, int r, int L, int R, int k) {
    // 如果整个区间已经处于目标状态，直接返回
    if (contained_count[x] == k * (r - l + 1))
      return;

    // 检查是否可以一次性翻转整个区间
    bool can_flip_entire_interval = false;

    if (k == 1) {
      // 添加操作：只有当区间内所有集合都不包含该值时，才能一次性添加
      can_flip_entire_interval = (contained_count[x] == 0);
    } else {
      // 删除操作：只有当区间内所有集合都包含该值时，才能一次性删除
      can_flip_entire_interval = (contained_count[x] == r - l + 1);
    }

    // 如果当前区间完全包含在操作区间内，且可以一次性翻转
    if (L <= l && r <= R && can_flip_entire_interval) {
      // 更新元素存在状态
      puttag(x, l, r, k);
      // 更新集合大小统计：添加元素时+1，删除元素时-1
      set_size_tree.modify(1, 1, n, l, r, (k == 1) ? 1 : -1);
      return;
    }

    pushdown(x, l, r);
    int mid = (l + r) >> 1;
    if (L <= mid)
      modify(lson, l, mid, L, R, k);
    if (R > mid)
      modify(rson, mid + 1, r, L, R, k);
    pushup(x);
  }
} element_existence_trees[60]; // 60个线段树，对应x的取值范围1-60

int main() {
  cin >> n >> Q;
  set_size_tree.Build(1, 1, n); // 初始化集合大小统计线段树
  for (int i = 0; i < 60; ++i)
    element_existence_trees[i].Build(1, 1, n); // 初始化60个元素存在状态线段树

  while (Q--) {
    int type, l, r;
    cin >> type >> l >> r;

    if (type == 1) {
      // 类型1：添加元素x到区间[l,r]的所有集合
      int x;
      cin >> x;
      x--;                                                 // 转换为0-based索引
      element_existence_trees[x].modify(1, 1, n, l, r, 1); // 设置包含状态为1
    } else if (type == 2) {
      // 类型2：从区间[l,r]的所有集合中删除元素x
      int x;
      cin >> x;
      x--;                                                 // 转换为0-based索引
      element_existence_trees[x].modify(1, 1, n, l, r, 0); // 设置包含状态为0
    } else if (type == 3) {
      // 类型3：查询区间[l,r]内集合元素数量的最大值和达到该最大值的集合个数
      pair<int, int> ret = set_size_tree.Query(1, 1, n, l, r);
      cout << ret.first << " " << ret.second << endl;
    }
  }
  return 0;
}